package com.nulldev.util.web.nTube.pages;

import static com.nulldev.util.web.nTube.pages.Urlizer.decodeUrlizer;
import static com.nulldev.util.web.nTube.pages.Urlizer.urlizer;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Map;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.RandomUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.codecs.uniapi.UniBase64;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.data.Base64.Base64;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.experiments.Experiments;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpClientFlags;
import com.nulldev.util.web.HttpClient.ResponseFormat;
import com.nulldev.util.web.HttpClient.clients.legacy.LegacyHttpRequest;
import com.nulldev.util.web.HttpServer3.ByteReadRange;
import com.nulldev.util.web.HttpServer3.ByteReadRangeParser;
import com.nulldev.util.web.HttpServer3.Response;
import com.nulldev.util.web.HttpServer3.pages.v3.ASyncHttpPage3;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLQueryParser;
import com.nulldev.util.web.URLUtils.URLQueryParser.URLQueryParserOptions;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.nTube.util.NTubeUtils;
import com.nulldev.util.web.nTubeExtensions.gvc_spoofer.GVCSpoofer;
import com.nulldev.util.web.nTubeExtensions.ntube_proxy.ProxyDoFunction;

/**
 * Proxy API Page (asynchronous variant) <br>
 * This page is used to proxy all traffic through nTube acting as a layer of
 * security and allowing us to bypass CORS, also fuck CORS by the way.
 * 
 * @author null
 * @since 1.5.0 (dev build 140)
 */
public class ProxyAPIAsyncPage extends ASyncHttpPage3 {

	public static final boolean USE_COPYABLE = Experiments.get("ntube://proxy-use-copyable/").isActive() || IOUtils.canUseDMA();
	public final static Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--ProxyAPI.debug"), true);

	/** Fixed timeout for the proxy, in seconds. */
	private static final int TIMEOUT_WAIT = 34;

	private static final boolean BASE_DEBUGGING = Arguments.hasArgument("--ProxyAPI.debugBasic");
	private static final boolean ENABLE_MODERN_TRANSPORT = Experiments.get("ntube://proxy-use-modern-transport/").isActive();
	private static final boolean ENABLE_SECURITY_HARDENING = Experiments.get("ntube://enable-security-hardening/").isActive();
	private static final boolean USE_CONTENT_RANGE_ON_ALL = Experiments.get("ntube://use-content-range-on-all/").isActive();
	private static final boolean USE_OK_CHUNKING = Experiments.get("ntube://okhttp-proxy-chunking/").isActive();
	private static final boolean BEHAVE_AS_GATEWAY = Experiments.get("ntube://proxy-as-gateway/").isActive();
	private static final boolean ENABLE_PROXY_BUFFERING = Experiments.get("ntube://proxy-enable-buffering/").isActive();

	private static final String[] ALLOWED_DOMAINS = new String[]
		{ ".*.googlevideo.com", "signaler-pa.youtube.com", "www.youtube.com", "youtube.com", "usher.twitch.tv", ".*.ttvnw.net", "fuck.advertising.local" };

	public ProxyAPIAsyncPage() {
		super("/proxyAPI");
	}

	private static LocalProcessingRequest request(final String url, final int bufferSize, final Entry... entries) throws Exception {
		return request(url, Proxy.NO_PROXY, bufferSize, entries);
	}

	private static LocalProcessingRequest request(final String url, final Proxy proxy, final int bufferSize, final Entry... entries) throws Exception {
		return new LocalProcessingRequest(HttpClient.getStream().url(url).timeout(Duration.ofSeconds(TIMEOUT_WAIT)).enableBuffering(bufferSize).headers(entries)
				.proxy(proxy).tryFeatureFlag(HttpClientFlags.FEATURE_FLAG_OK_FORCE_CHUNKING, USE_OK_CHUNKING).create());
	}

	@SuppressWarnings(
		{ "unused" })
	private static LocalProcessingRequest requestLegacy(final String url, final Proxy proxy, final Entry... entries) throws Exception {
		return new LocalProcessingRequest((ENABLE_MODERN_TRANSPORT ? HttpClient.getStream() : (new LegacyHttpRequest<InputStream>() {

			@Override
			public ResponseFormat getFormat() {
				return ResponseFormat.STREAM;
			}
		})).url(url).headers(entries).proxy(proxy).enableBuffering(IOUtils.MEMORY_ALLOC_BUFFER).timeout(Duration.ofSeconds(TIMEOUT_WAIT)).create());
	}

	private static final byte[] WEBKIT_INIT_WORKAROUND = new byte[]
		{ '#', 'E' };

	@SuppressWarnings("resource")
	private void __youtubeHLS(final String url, final ControllableFutureTask<Response> r, final boolean usingProxy, final int bufferSize,
			final Map<String, String> s, final String _intUrl, final Request req) {
		try {
			final ByteArrayOutputStream baos = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
			if (req.headerIs("Range", "bytes=0-1")) {
				// I fucking hate WebKit.
				baos.write(WEBKIT_INIT_WORKAROUND);
				baos.flush();
				r.complete(new Response(206, new ByteArrayInputStream(baos.toByteArray())).requestKeepAliveDisable(true).header("Content-Range", "bytes 0-1/*")
						.header("Content-Length", "2").header("Content-Type", "application/vnd.apple.mpegurl")
						.header("NT-ProxyAPI-Handler", "YTHLS_WORKAROUND"));
				return;
			}
			final String yurl = url.replace("?ythls=", "");
			log.debug("ythls_url: " + yurl);
			final LocalProcessingRequest lpr = request(yurl, bufferSize);
			if (lpr.code() == 403) {
				log.error("[ProxyAPI::youtubeHLS] Got Error 403 for: " + yurl);
				r.complete(new Response(403).requestKeepAliveDisable(true));
				return;
			}
			final BufferedReader br = new BufferedReader(new InputStreamReader(lpr.getResponse()), IOUtils.MEMORY_ALLOC_BUFFER);
			String line;
			while ((line = br.readLine()) != null) {
				final String res = line;
				if (!res.startsWith("http://") && !res.startsWith("https://") && !res.startsWith("/proxyAPI") && !res.startsWith("#EXT-X-MEDIA:URI")
						&& !res.startsWith("#EXT-X-ENDLIST")) {
					baos.write((res + "\n").getBytes());
					baos.flush();
				} else if (res.startsWith("#EXT-X-MEDIA:URI")) {
					final String uri = StringsUtil.substringBetweenO(res, "URI=\"", "\",");
					final String uri_urlizer = _intUrl + urlizer(uri) + (uri.endsWith(".m3u8") ? "&ythls" : "");
					baos.write((StringsUtil.replace(res, uri, uri_urlizer) + "\n").getBytes());
					baos.flush();
				} else if (res.startsWith("#EXT-X-ENDLIST")) {
					/* Workaround for requests timing out... */
					baos.write((res + "\n").getBytes());
					baos.flush();
					break;
				} else {
					boolean redir = false;
					if (res.endsWith(".m3u8")) {
						redir = true;
					}
					String o = _intUrl + urlizer(res);
					if (usingProxy) {
						if (s.containsKey("psocks")) {
							o += "&psocks";
						}
						o += "&pip=" + s.get("pip");
						o += "&pp=" + s.get("pp");
					}
					if (redir) {
						o += "&ythls";
					}
					baos.write((o + "\n").getBytes());
					baos.flush();
					o = null;
				}
			}
			lpr.close();
			r.complete(new Response(lpr.code(), new ByteArrayInputStream(baos.toByteArray())).header("Accept-Ranges", "bytes")
					.header("Content-Type", "application/vnd.apple.mpegurl").header("NT-ProxyAPI-Handler", "YTHLS").requestKeepAliveDisable(true));
			baos.close();
			br.close();
			return;
		} catch (Throwable e) {
			if (NTubeUtils.isSlientNetworkException(e)) {
				log.error("Request failed for: " + url);
			} else {
				log.error(e);
			}
			r.complete(new Response(BEHAVE_AS_GATEWAY ? 504 : 500, createStream("PLEASE RELOAD, I HAVE NO CLUE WHY IT BROKE.")).requestKeepAliveDisable(true)
					.header("Retry-After", "0"));
			return;
		}
	}

	private static final String FADVERTISING_PAYLOAD = "" + "#EXTM3U\n" + "#EXT-X-VERSION:3\n" + "#EXT-X-TARGETDURATION:5\n" + "#EXT-X-MEDIA-SEQUENCE:0\n"
			+ "#EXT-X-SCTE35-OUT:URL=\"http://fuck.advertising.local/ad_blocked\"\n" + "#EXT-X-DISCONTINUITY-SEQUENCE:0\n" + "#EXT-X-DISCONTINUITY\n"
			+ "#EXTINF:5.000,fuckAdvertising_for_Twitch\n" + "/resources/fa/ad_blocked.ts\n";

	@SuppressWarnings(
		{ "resource" })
	@Override
	public void onFutureRequest(final Request request, final ControllableFutureTask<Response> r) {
		final Map<String, String> s = URLQueryParser.extract(request.url(), URLQueryParserOptions.PREFER_EXPERIMENTAL,
				(URLUtils.USE_URL_ACCEL_V2 ? null : URLQueryParserOptions.DISABLE_URL_ACCEL), URLQueryParserOptions.DISABLE_DECODING,
				URLQueryParserOptions.TREAT_URL_AS_QUERY);
		if (!s.containsKey("url") && !(s.containsKey("uzm") && s.containsKey("uzc"))) {
			r.complete(new Response(400).header("Proxy-Rejected", "INVALID_URLIZER").requestKeepAliveDisable(true));
			return;
		} else if (s.containsKey("url") && s.get("url").isEmpty()) {
			r.complete(new Response(400).header("Proxy-Rejected", "MISSING_URL").requestKeepAliveDisable(true));
			return;
		}
		final boolean usingProxy = s.containsKey("pip") && s.containsKey("pp");
		final boolean treatAsB64 = s.containsKey("treatAsB64");

		String url = decodeUrlizer(request.url());

		final boolean isTranslated = url.contains("googlevideo.com") && url.contains("%2Fdash_translate%2FproxyAPI//");
		final boolean forceLegacy = s.containsKey("forceLegacy") || request.hasHeader("X-YouTube-Ad-Signals");
		log.debug("decoded-url: " + url);
		if (isTranslated)
			url = url.replace("%2Fdash_translate%2FproxyAPI//", "");

		final boolean ytlive = s.containsKey("ytlive");
		final boolean youtubeHLS = s.containsKey("ythls") && !url.endsWith(".ts") && url.contains("googlevideo.com");
		final boolean youtubeLiveVideo = url.contains("googlevideo.com") && url.contains("seg.ts");
		final boolean youtubeVideo = !youtubeHLS && url.contains("googlevideo.com") && !url.contains("seg.ts");
		if ((treatAsB64) || !url.startsWith("http") || Base64.isBase64(url)) {
			url = UniBase64.decodeToString(url);
		}

		/* nTube workaround for legacy WebKit */
		final String force_clen = s.containsKey("_fclen") ? s.get("_fclen") : null;

		final boolean privacy_noRequestCountTracking = s.containsKey("privacy_nrct");
		if (ENABLE_SECURITY_HARDENING && !ArrayUtils.StringArrayUtils.cardedContains(ALLOWED_DOMAINS, URLUtils.getDomain(url))) {
			log.warn("Blocking invalid URL: " + url);
			r.complete(new Response(401).requestKeepAliveDisable(true));
			return;
		}
		final int bufferSize = ENABLE_PROXY_BUFFERING ? IOUtils.MEMORY_ALLOC_BUFFER : -1;
		Proxy p = Proxy.NO_PROXY;
		if (usingProxy) {
			p = new Proxy((s.containsKey("psocks") ? Type.SOCKS : Type.HTTP), new InetSocketAddress(s.get("pip"), Integer.parseInt(s.get("pp"))));
		}

		final boolean hasRange = request.getHeaders().containsKey("Range")
				&& !request.getHeaders().getOrDefault("Range", "bytes=0-").equalsIgnoreCase("bytes=0-");

		final String _intUrl;
		/* HttpServer mandates the Host header from 3.0.6 */
		final String _hdr = request.header("Host", "");
		if (!StringsUtil.isZeroLengthOrNull(_hdr)) {
			if (NTubeUtils.wasOriginHTTPS(request)) {
				_intUrl = "https://" + _hdr;
			} else {
				_intUrl = "http://" + _hdr;
			}
		} else {
			_intUrl = _hdr;
		}

		if (log.isDebugEnabled()) {
			log.debug("Request Headers:");
			ArrayUtils.printLln(request.getHeaders());
		}

		final String fullUrl;
		final String _range;
		final ByteReadRange[] byteRanges;
		final ArrayList<Entry> customHeaders = /* __unlikely__ */ new ArrayList<Entry>(0);

		if (youtubeHLS && !ytlive) {
			__youtubeHLS(url, r, usingProxy, bufferSize, s, _intUrl, request);
			return;
		} else if (url.equals("http://fuck.advertising.local/block.m3u8")) { // OK: DO-NOT-CHANGE
			r.complete(new Response(200, createStream(FADVERTISING_PAYLOAD)).header("Accept-Ranges", "bytes"));
			return;
		} else if (hasRange) {
			_range = request.getHeaders().get("Range");
			byteRanges = ByteReadRangeParser.parse(_range);
			String formattedUrl = youtubeVideo ? GVCSpoofer.generateSpoofedQueries(url, byteRanges, privacy_noRequestCountTracking) : url;

			final boolean requires_hls_workaround = __requires_hls_workaround(byteRanges);
			if (youtubeVideo) {
				if (byteRanges.length == 1 && !requires_hls_workaround) {
					formattedUrl += "&range=" + byteRanges[0].format();
				} else if (requires_hls_workaround) {
					/*
					 * YouTube refuses to send a Content-Length header if we use the &range= query.
					 */
					customHeaders.add(new Entry("Range", _range));
				} else {
					log.warn("Invalid Range Header: " + _range);
					r.complete(new Response(400).header("Proxy-Rejected", "INVALID_RANGES").requestKeepAliveDisable(true).header("Retry-After", "0")
							.header("Accept-Ranges", "bytes"));
					return;
				}
			}
			fullUrl = formattedUrl;
			if (!fullUrl.contains("&sig=") && youtubeVideo) {
				log.warn("Invalid YouTube URL: " + fullUrl);
				r.complete(new Response(400).header("Proxy-Rejected", "INVALID_YT_URL").requestKeepAliveDisable(true).header("Retry-After", "0")
						.header("Accept-Ranges", "bytes"));
				return;
			}
		} else {
			_range = null;
			byteRanges = null;
			fullUrl = url;
		}
		final int maxRetries = 2;
		int tryCount = 0;
		boolean success = false;
		for (; tryCount < maxRetries; tryCount++) {
			try {
				success = true;
				if (ProxyDoFunction.doRegularProxy(customHeaders, r, fullUrl, request, forceLegacy, youtubeVideo, youtubeHLS, privacy_noRequestCountTracking, p,
						bufferSize, youtubeLiveVideo, tryCount, byteRanges, force_clen))
					break;
			} catch (Exception e) {
				success = false;
				if (NTubeUtils.isSlientNetworkException(e)) {
					log.error("nTube.proxy -> Request failed!", e.getMessage());
				} else {
					log.error("nTube.proxy -> Request failed!", e);
				}
				continue;
			}
		}
		if (tryCount >= maxRetries && !success) {
			r.complete(new Response(BEHAVE_AS_GATEWAY ? 502 : 500).requestKeepAliveDisable(true).header("Retry-After", "0").header("Accept-Ranges", "bytes"));
			return;
		}
	}

	private static boolean __requires_hls_workaround(final ByteReadRange[] brr) {
		/* WebKit is hellbent on doing a Range=[0, 1] to identify the Content-Length */
		return brr != null && brr.length == 1 && brr[0].min == 0 && brr[0].max == 1;
	}

	private static final long UNQ_VAL = RandomUtil.randomLong();

	@Override
	public String pc_get_uuid() {
		return "NTUBE-PROXY-PROTECTION-" + UNQ_VAL;
	}
}